/*

	Copyright 2010 Anatol Gregory Mayen
	
	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at
 
	http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
*/
package eu.maydu.gwt.gxtcomponents.client.password;


import java.util.LinkedList;
import java.util.List;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.Style.HideMode;
import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
import com.extjs.gxt.ui.client.Style.Orientation;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.core.XTemplate;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.FieldEvent;
import com.extjs.gxt.ui.client.event.KeyListener;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.util.Format;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.util.Util;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ComponentHelper;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Html;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.WidgetComponent;
import com.extjs.gxt.ui.client.widget.Window;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.AdapterField;
import com.extjs.gxt.ui.client.widget.form.Field;
import com.extjs.gxt.ui.client.widget.layout.CardLayout;
import com.extjs.gxt.ui.client.widget.layout.CenterLayout;
import com.extjs.gxt.ui.client.widget.layout.FillLayout;
import com.extjs.gxt.ui.client.widget.layout.RowData;
import com.extjs.gxt.ui.client.widget.layout.RowLayout;
import com.extjs.gxt.ui.client.widget.layout.TableLayout;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Widget;

import eu.maydu.gwt.gxtcomponents.client.password.PasswordStrengthRule.RuleClass;
import eu.maydu.gwt.gxtcomponents.client.password.i18n.PasswordI18n;


/**
 * A password strength indicator.
 * It can be configured to specify what a strengthened password
 * is. It is attached to an input widget, where the individual 
 * keystrokes are then captured, updating the actual strength of
 * the input. The actual strength of the input can be shown via
 * a image and alternatively or additionally through text.
 * 
 * @author amayen
 *
 */
public class PasswordStrengthIndicator extends AdapterField {
	
	private LayoutContainer panel = new LayoutContainer();
	private Field input;
	private int maxPoints = -1;
	private boolean showDescription = true;
	private PasswordI18n i18n = GWT.create(PasswordI18n.class);
	
	private List<PasswordStrengthRule> rules = new LinkedList<PasswordStrengthRule>();
	
	private String ALPHABET_IGNORECASE_CHARS = "";
	
	private String label = i18n.passwordStrengthIndicatorPasswordStrengthText();
	private String whatsMissingText = i18n.passwordStrengthIndicatorWhatsMissingText();
	private Anchor whatsMissing = new Anchor(whatsMissingText);
	private ContentPanel indicatorPanels[];
	private LayoutContainer helpContainer;
	private Window win;
	
	private boolean rulesFulfilled = false;
	private List<PasswordStrengthRule> unfulfilledRules = new LinkedList<PasswordStrengthRule>();
	
	private Component helpWindowComponent = null;
	private CardLayout helpWindowCardLayout;
	private LayoutContainer mainHelpContainer;
	private boolean showHelpWinModal = false;
	private String messageTarget = "side";
	
	public PasswordStrengthIndicator(Field input) {
		super(null);
		this.input = input;
		setupInput();
		setupWhatsMissing();
		panel.setLayout(new RowLayout());
		setFieldLabel(label);
		panel.add(buildIndicator(), new RowData(1, 20, new Margins(5, 0, 0, 0)));
		panel.add(whatsMissing, new RowData(120, Style.DEFAULT, new Margins(5, 0, 0, 0)));
		widget = panel;
		//setSize(panel.getWidth(), panel.getHeight());
	}
	
	public PasswordStrengthIndicator(Field input, Component helpWindowComponent) {
		this(input);
		this.helpWindowComponent = helpWindowComponent;
		setupHelpContainer();
	}
	
	public PasswordStrengthIndicator(Field input, PasswordI18n i18n) {
		this(input);
		this.i18n = i18n;
	}
	
	public PasswordStrengthIndicator(Field input, Component helpWindowComponent, PasswordI18n i18n) {
		this(input);
		this.i18n = i18n;
		this.helpWindowComponent = helpWindowComponent;
	}
	
	public void setShowModal(boolean m) {
		this.showHelpWinModal = m;
	}
	
	private void setupHelpContainer() {
		helpWindowCardLayout = new CardLayout();
		mainHelpContainer = new LayoutContainer(helpWindowCardLayout);
		helpContainer = new LayoutContainer();
		
		TableLayout l = new TableLayout(3);
		l.setCellPadding(3);
		l.setCellHorizontalAlign(HorizontalAlignment.CENTER);
		helpContainer.setLayout(l);
		
		
		LayoutContainer container = new LayoutContainer();
		RowLayout rowLayout = new RowLayout();
		container.setLayout(rowLayout);
		container.add(helpContainer, new RowData(1, 140/*, new Margins(0, 0, 50, 0)*/));
		if(helpWindowComponent != null) {
			container.add(helpWindowComponent, new RowData(1, Style.DEFAULT, new Margins(10, 0, 0 ,0)));
		}
		
		LayoutContainer successContainer = new LayoutContainer(new CenterLayout());
		Html t = new Html("<br/><br/><br/><br/><br/><br/><br/><br/><br/>"+i18n.passwordStrengthIndicatorPasswordIsStrong());
		t.setStyleAttribute("color", "green");
		t.setStyleAttribute("font-weight", "bold");
		successContainer.add(t);
		
		container.setSize(700, 200);
		
		mainHelpContainer.add(successContainer);
		mainHelpContainer.add(container);
		
		helpWindowSwitchToSuccess();
		
		win = new Window();
		win.setModal(false);
		win.setHeading(whatsMissingText);
		win.add(mainHelpContainer);
		Button close = new Button(i18n.passwordStrengthIndicatorHelpWindowCloseButton());
		close.addSelectionListener(new SelectionListener<ButtonEvent>() {

			@Override
			public void componentSelected(ButtonEvent ce) {
				win.hide();
			}
			
		});
		win.addButton(close);
		win.setClosable(true);
		win.setSize(670, 250);
	}

	private void helpWindowSwitchToSuccess() {
		helpWindowCardLayout.setActiveItem(mainHelpContainer.getItem(0));
	}
	
	private void helpWindowSwitchToFailure() {
		helpWindowCardLayout.setActiveItem(mainHelpContainer.getItem(1));
	}

	private void setupWhatsMissing() {
		whatsMissing.setVisible(false);
		whatsMissing.addClickHandler(new ClickHandler() {

			@Override
			public void onClick(ClickEvent event) {
				setupHelpContainer();
				invokeCalculation();
				showTipWindow();
			}
			
		});
	}

	private void showTipWindow() {
		win.setModal(showHelpWinModal);
		win.show();
	}

	private Widget buildIndicator() {
		LayoutContainer container = new LayoutContainer(new RowLayout(Orientation.HORIZONTAL));
		
		indicatorPanels = new ContentPanel[8];
		
		for(int i=0; i< indicatorPanels.length; i++) {
			ContentPanel p = new ContentPanel();
			p.setLayout(new FillLayout());
			p.setBorders(true);
			p.setBodyBorder(false);
			p.setHeaderVisible(false);
			
			indicatorPanels[i] = p;
			container.add(p, new RowData(25, 12));
		}
		return container;
	}

	private void setupInput() {
		if(input == null)
			throw new RuntimeException("The input field must not be null!");
		
		input.addKeyListener(new KeyListener() {

			@Override
			public void componentKeyUp(ComponentEvent ce) {
				invokeCalculation();
			}
		});
	}
	
	private void invokeCalculation() {
		Object value  = input.getValue();
		if(value == null)
			value = "";
		calcPasswordStrength(value.toString());
	}
	
	private void calcPasswordStrength(String value) {
		
		int actPoints = 0;
		initializePoints();
		unfulfilledRules.clear();
		for(PasswordStrengthRule rule : rules) {
			int points = checkRule(rule);
			if(points == 0)
				unfulfilledRules.add(rule);
			actPoints += points;
		}
		
		updatePasswordStrength(actPoints);
	}
	
	private void updatePasswordStrength(int actPoints) {
		float percent = (float)actPoints/maxPoints*100;
		//Window.alert("Passed: "+percent);
		
		int count;
		String color = "";
		rulesFulfilled = false;
		whatsMissing.setVisible(true);
		if(percent == 100.0) {
			color = "green";
			count = indicatorPanels.length;
			rulesFulfilled = true;
			whatsMissing.setVisible(false);
			clearInvalid();
		}else if(percent >= 75.0) {
			color = "yellow";
			count = (int)(indicatorPanels.length * 0.75);
		}else if(percent >= 50.0) {
			color = "orange";
			count = indicatorPanels.length / 2;
		}else if(percent >= 25.0) {
			color = "red";
			count = indicatorPanels.length / 4;
		}else {
			count = 0;
		}
		
		for(int i=0; i<count; i++) {
			indicatorPanels[i].getBody().applyStyles("backgroundColor:"+color+";");
		}
		for(int i=count; i<indicatorPanels.length; i++) {
			indicatorPanels[i].getBody().applyStyles("backgroundColor:white;");
		}
		
		updateHelpWindow();
	}

	private void updateHelpWindow() {
		helpContainer.removeAll();
		if(unfulfilledRules.isEmpty()) {
			helpWindowSwitchToSuccess();
		}else {
			helpWindowSwitchToFailure();

			XTemplate template = XTemplate.create(getTemplate());
			
			helpContainer.removeAll();
			GWT.log("unfulfilledCount: "+unfulfilledRules.size());
			helpContainer.addText(template.applyTemplate(Util.getJsObject(new UnfulfilledPasswordStrengthRules(unfulfilledRules, i18n), 1)));
		}
		win.layout();
		
	}
	
	private native String getTemplate() /*-{ 
		var html = [ 
		'<table border="0" cellspacing="5" width="100%">', 
		'<tr>', 
		'<td align="center" style="font-weight:bold;" width="300">{ruleDescriptionHeader}</td>',
		'<td align="center" style="font-weight:bold;">{ruleCharacterSetHeader}</td>',
		'<td align="center" style="font-weight:bold;">{ruleMinCountHeader}</td>', 
		'</tr>', 
		'<tpl for="rules">', 
		'<tr>',
		'<td>{description}</td>',
		'<td align="center">{charSet}</td>',
		'<td align="center">{minCount}</td>',
		'</tr>',
		'</tpl>', 
		'</table>'
		]; 
		return html.join(""); 
	}-*/;  
	
	
	private int checkRule(PasswordStrengthRule rule) {
		
		Object v = input.getValue();
		if(v==null)
			v = "";
		String value = v.toString();
		if(rule.getRuleClass() == RuleClass.PASSWORD_LENGTH) {
			return value.length() >= rule.getRuleFulfilledCount()? rule.getRuleFulfilledPoints() : 0;
		}
		String ruleChars = rule.getRuleChars();
		
		int count = charsInsideStringCount(ruleChars, value);
		if(count >= rule.getRuleFulfilledCount())
			return rule.getRuleFulfilledPoints();
		else return 0;
	}

	private void initializePoints() {
		
		if(maxPoints == -1) {
			//maxPoints was not initialized, so we create a
			//default point system where each rule is worth
			//one point and the max points is the count of
			//rules existing, meaning each rule must be fulfilled
			maxPoints = rules.size();
			for(PasswordStrengthRule rule: rules) {
				rule.setRuleFulfilledPoints(1);
			}
		}
	}

	public void addPasswordStrengthRule(PasswordStrengthRule rule) {
		if(rules.contains(rule))
			throw new RuntimeException("Rule of class: "+rule.getRuleClass().name()+" was already added to rules");
		rules.add(rule);
	}
	
	
	private int charsInsideStringCount(String charsToCount, String input) {
		int count = 0;
		for(int i=0; i< charsToCount.length(); i++) {
			for(int j=0; j< input.length(); j++) {
				if(charsToCount.charAt(i) == input.charAt(j))
					count++;
			}
		}
		return count;
	}
	
	public boolean validateValue(String value) {
		if(!isValid()) {
			markInvalid(i18n.passwordStrengthIndicatorValidatorNotValid());
			return false;
		}
		return true;
	}
	
	public boolean isValid(boolean silent) {
		if (disabled)
			return true;
		
		boolean result = validateValue(getRawValue());
		return result;
	}
	
	public boolean isValid() {
		
		return rulesFulfilled;
	}
	
	public boolean validate() {
		return isValid(false);
	}
	
	public void setMessageTarget(String target) {
		if(target.equals("side") || target.equals("title") 
				|| target.equals("tooltip") || target.equals("none"))
			messageTarget = target;
	}
	
	public void markInvalid(String msg) {
	    msg = Format.htmlEncode(msg == null ? getMessages().getInvalidText() : msg);
	    if (!rendered || preventMark) {
	      return;
	    }
	    //getInputEl().addStyleName(invalidStyle);

	    if ("side".equals(messageTarget)) {
	      if (errorIcon == null) {
	        errorIcon = new WidgetComponent(getImages().getInvalid().createImage());
	        Element p = el().getParent().dom;
	        errorIcon.render(p);
	        errorIcon.setHideMode(HideMode.VISIBILITY);
	        errorIcon.hide();
	        errorIcon.setStyleAttribute("display", "block");
	        errorIcon.el().makePositionable(true);
	      } else if (!errorIcon.el().isConnected()) {
	        Element p = el().getParent().dom;
	        p.appendChild(errorIcon.getElement());
	      }
	      if (!errorIcon.isAttached()) {
	        ComponentHelper.doAttach(errorIcon);
	      }

	      alignErrorIcon();
	      if (GXT.isIE || GXT.isOpera) {
	        alignErrorIcon();
	      }
	      // needed to prevent flickering
	      DeferredCommand.addCommand(new Command() {
	        public void execute() {
	          if (errorIcon.isAttached()) {
	            errorIcon.show();
	          }
	        }
	      });
	      errorIcon.setToolTip(msg);
	      errorIcon.getToolTip().addStyleName("x-form-invalid-tip");
	      el().repaint();
	    } else if ("title".equals(messageTarget)) {
	      setTitle(msg);
	    } else if ("tooltip".equals(messageTarget)) {
	      setToolTip(msg);
	      getToolTip().addStyleName("x-form-invalid-tip");
	      getToolTip().enable();
	    } else if ("none".equals(messageTarget)) {
	      // do nothing
	    } else {
	      Element elem = XDOM.getElementById(messageTarget);
	      if (elem != null) {
	        elem.setInnerHTML(msg);
	      }
	    }

	    FieldEvent fe = new FieldEvent(this);
	    fe.setMessage(msg);
	    fireEvent(Events.Invalid, fe);
	  }
	
	  /**
	   * Clear any invalid styles / messages for this field.
	   */
	  public void clearInvalid() {
	    if (!rendered) {
	      return;
	    }
//	    getInputEl().removeStyleName(invalidStyle);

	    if (forceInvalidText != null) {
	      forceInvalidText = null;
	    }

	    if ("side".equals(messageTarget)) {
	      if (errorIcon != null && errorIcon.isAttached()) {
	        ComponentHelper.doDetach(errorIcon);
	        errorIcon.setVisible(false);
	      }
	    } else if ("title".equals(messageTarget)) {
	      setTitle("");
	    } else if ("tooltip".equals(messageTarget)) {
	      hideToolTip();
	      if (toolTip != null) {
	        toolTip.disable();
	      }
	    } else {
	      Element elem = XDOM.getElementById(messageTarget);
	      if (elem != null) {
	        elem.setInnerHTML("");
	      }
	    }
	    fireEvent(Events.Valid, new FieldEvent(this));
	  }

}